package net.fukure.android.pecabc.media;

import java.nio.BufferOverflowException;
import java.nio.ShortBuffer;
import java.util.ArrayList;

import net.fukure.android.pecabc.R;
import net.fukure.android.pecabc.listener.OnAudioListener;
import net.fukure.android.pecabc.listener.OnEncodeListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;

import com.googlecode.javacv.FFmpegFrameRecorder;
import com.googlecode.javacv.cpp.opencv_core.CvPoint2D32f;
import com.googlecode.javacv.cpp.opencv_core.IplImage;
import com.googlecode.javacv.cpp.opencv_core.CvMat;

import static com.googlecode.javacv.cpp.avcodec.AV_CODEC_ID_WMV1;
import static com.googlecode.javacv.cpp.avcodec.AV_CODEC_ID_WMAV1;
import static com.googlecode.javacv.cpp.opencv_core.CV_8UC1;
import static com.googlecode.javacv.cpp.opencv_core.CV_8UC3;
import static com.googlecode.javacv.cpp.opencv_core.CV_32FC1;
import static com.googlecode.javacv.cpp.opencv_core.IPL_DEPTH_8U;
import static com.googlecode.javacv.cpp.opencv_core.cvCreateMat;
import static com.googlecode.javacv.cpp.opencv_imgproc.CV_YUV2BGR_NV21;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvCvtColor;
import static com.googlecode.javacv.cpp.opencv_imgproc.cv2DRotationMatrix;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvWarpAffine;
import static com.googlecode.javacv.cpp.opencv_imgproc.cvGetRectSubPix;

public class Encoder implements OnAudioListener {
	
    private final static String LOG_TAG = "AndroidFFWMEEncoder";
    private final static String TIME_TAG = "RecoderTimestamp";
        
	private boolean recording = false;
	private boolean encording = false;
	private boolean encoderPrintTimestamp = false;
	private long startTime = 0;
	private int errorCount = 0;
	private byte[] previewBuffer;
	private EncodeParam param = null;
	private IplImage iplimage = null;
	private IplImage nullIplimage;
	private Context context = null;
	private AudioRecoder audio = null;
	private FFmpegFrameRecorder recorder = null;
	private Thread videoEmptyRecordThread = null;
	private ArrayList<OnEncodeListener> listeners = new ArrayList<OnEncodeListener>();
	private ArrayList<Long> fpsc = new ArrayList<Long>();
    public static int encodeTypeNum = 0;

    private CvMat cvMatNv21Image = null;
    private CvMat cvMatBgrImage = null;
    private CvMat cvMatCropImage = null;
    private CvMat cvMatBgrRotImage = null;
	
	public Encoder(Context context) {
		this.context = context;
		this.audio = new AudioRecoder();
		this.audio.addAudioListener(this);
	}
	
	public void release(){
		audio.release();
	}
	
	public boolean startRec(String url, EncodeParam param){
		if(recording) return true;
		int w,h;

		w = param.portrait==1 ? param.height : param.width;
		h = param.portrait==1 ? param.width : param.height;
		if(param.portrait==1){
			h = EncodeType.crop ? h/2 : h;
		}
		
		this.param = param;
		this.errorCount = 0;
		
		if(recorder==null){
			recorder = new FFmpegFrameRecorder(url, w, h);
			recorder.setFormat("asf");
			recorder.setSampleRate(audio.getSampleRate());
			recorder.setAudioChannels(audio.getAudioChannel());
			recorder.setFrameRate(param.framerate);
			recorder.setVideoCodec(AV_CODEC_ID_WMV1);
			recorder.setAudioCodec(AV_CODEC_ID_WMAV1);
			recorder.setVideoBitrate(param.bitrate*1024);
			recorder.setAudioBitrate(32000);
			/*
			recorder.setVideoCodec(AV_CODEC_ID_VC1);
			recorder.setVideoCodec(AV_CODEC_ID_VP6);
			recorder.setVideoCodec(AV_CODEC_ID_VP8);
			recorder.setVideoCodec(AV_CODEC_ID_H263);
			recorder.setPixelFormat(PIX_FMT_YUV420P);
			recorder.setPixelFormat(PIX_FMT_NV21);
			*/
		}
		Log.i(LOG_TAG,"rec start");
		Log.d(LOG_TAG,"video bitrate "+param.bitrate);
		Log.d(LOG_TAG,"audio codec "+recorder.getAudioCodec());
		Log.d(LOG_TAG,"video codec "+recorder.getVideoCodec());
		Log.d(LOG_TAG,"recoder size "+w+","+h);
		
		if(iplimage!=null){
			iplimage.release();
		}
		iplimage = IplImage.create(w, h, IPL_DEPTH_8U, 2);
		
        try {
	        recording = true;
			recorder.start();
	        startTime = System.currentTimeMillis();
        	audio.start();
        	startVideoEmptyRecordThread();
    		Log.d(LOG_TAG,"rec start");
    		return true;
	    } catch (FFmpegFrameRecorder.Exception e) {
			stopRec();
			Log.d(LOG_TAG,"rec error "+e.getMessage(), e);
	    }
		return false;
	}

	public boolean stopRec(){
    	if(!recording) return true;
        recording = false;
        stopVideoEmptyRecordThread();
        try {
            audio.stop();
            Thread.sleep(1000);
        } catch (Exception e) {
			Log.e(LOG_TAG,"rec stop error "+e.getMessage());
        }
        try{
            recorder.stop();
            recorder.release();
        } catch (FFmpegFrameRecorder.Exception e) {
			Log.e(LOG_TAG,"rec stop error "+e.getMessage());
		}
        recorder = null;
        fpsc.clear();
		Log.d(LOG_TAG,"rec end");
		return true;
    }
	
	private void audioRec(ShortBuffer buffer){
		//if(System.currentTimeMillis()-startTime<1000) return;
		if(recorder!=null && recording){
			if(errorCount>5){
				if(errorCount==6){
					fireEncodeErrorEvent();
					stopRec();
				}
				return;
			}
			try {
				recorder.record(buffer);
				errorCount = 0;
			} catch (Exception e) {
				errorCount++;
				Log.e(LOG_TAG, "record audio error "+e.getMessage());
			}
		}
	}

	private void videoRec(byte[] c){
		//if(System.currentTimeMillis()-startTime<3000) return;
		if(recorder!=null && recording && !encording){
			if(errorCount>5){
				return;
			}
			VideoRecThread t = new VideoRecThread(c);
			t.start();
			
			fireFpsEvent(getFps());
		}
	}

	private void videoEmptyRec() {
		try{
			if(System.currentTimeMillis() - startTime < 5000) return;
			if(nullIplimage==null){
				int id = param.portrait==1 ? R.drawable.peercast_p : R.drawable.peercast_l;
				Bitmap bmp = ImageUtil.createBitmapFromResource(context, id);
				nullIplimage = ImageUtil.bmpToIplImage(bmp);
				bmp.recycle();
			}
			long timestamp = 1000 * (System.currentTimeMillis() - startTime);
	    	recorder.setTimestamp(timestamp);
			recorder.record(nullIplimage);
		}catch(Exception e){
			Log.e(LOG_TAG, "nullRec error "+e.getMessage());
		}
	}

	class VideoRecThread extends Thread{
		private byte[] previewRotBuffer;
		private byte[] cropdata;
		public VideoRecThread(byte[] b) {
			if(b==null) return;
			encording = true;
			if(previewBuffer==null || previewBuffer.length != b.length){
				previewBuffer = new byte[b.length];
			}
			System.arraycopy(b, 0, previewBuffer, 0, b.length);
		}
		private void frameSleep(){
			try {
				for (int i = 0; i < 10; i++) {
					if(param.framerate+10<getFps()){
						Thread.sleep(5);
					}else{
						break;
					}
				}
			} catch (InterruptedException e) {
			}
		}
		@Override
		public void run() {
			super.run();
			
			IplImage image = libconv();
	
			try {
				
				long timestamp = 1000 * (System.currentTimeMillis() - startTime);
		    	recorder.setTimestamp(timestamp);
				recorder.record(image);

				addFrameTime(System.currentTimeMillis());
				//frameSleep();
				
				if(encoderPrintTimestamp){
					Log.i(TIME_TAG, timestamp+"");
				}
			} catch (Exception e) {
				//Log.e(LOG_TAG, "record video error"+e.getMessage());
			}
			
			encording = false;
		}

		IplImage conv(){
			int w = param.width;
			int h = param.height;

			byte[] out = previewBuffer;
			
			if(param.portrait==1){
				if(previewRotBuffer==null || previewRotBuffer.length!=previewBuffer.length){
					previewRotBuffer = new byte[previewBuffer.length];
				}
				ImageUtil.rotateYUV420Degree90UpperHalf(out, previewRotBuffer, w, h);
				int i = w; w = h; h = i;
				out = previewRotBuffer;
				
				if(EncodeType.crop){
					if(cropdata==null || cropdata.length!=previewBuffer.length/2){
						cropdata = new byte[previewBuffer.length/2];
					}
		    		int y = w*h;
					int uv = w*h/2;
					//uvlen = uv/MainActivity.length;
					//int uvoffset = uv*MainActivity.offset/100;
					//if((y+uvoffset)+uvlen>=y+uv){
					//	uvlen = y+uv-(y+uvoffset);
					//}
					System.arraycopy(out, 0, cropdata, 0, y/2);
					System.arraycopy(out, y, cropdata, y/2, uv/2);
					//System.arraycopy(previewBuffer, y+uvoffset, cropdata, y/2+uvoffset, uvlen/2);
					h = h/2;
					out = cropdata;
				}
			}

			IplImage image = null;
			try {
				image = iplimage;
				image.getByteBuffer().put(out);
			} catch (BufferOverflowException e) {
				try {
					image = IplImage.create(w, h, IPL_DEPTH_8U, 2);
					image.getByteBuffer().put(out);
				} catch (BufferOverflowException e2) {
				}
			}
			
			return image;
		}
		
		
		IplImage libconv(){
	        byte[] data = previewBuffer;
	        
	        try{
	        	if(cvMatNv21Image==null){
	        		throw new Exception();
	        	}
				if(cvMatNv21Image.getByteBuffer().array().length != previewBuffer.length){
					throw new Exception();
				}
	        }catch(Exception e){
	        	if(cvMatNv21Image!=null){
	        		cvMatNv21Image.release();
	        		cvMatBgrImage.release();
	        		cvMatCropImage.release();
	        		cvMatBgrRotImage.release();
	            }
	    		cvMatNv21Image = CvMat.create(param.height + (param.height/2), param.width, CV_8UC1);
				cvMatBgrImage = CvMat.create(param.height, param.width, CV_8UC3);
				cvMatCropImage = CvMat.create(param.height, param.width/2, CV_8UC3);
				cvMatBgrRotImage = CvMat.create(param.width/2, param.height, CV_8UC3);
	        }
	        
	        cvMatNv21Image.getByteBuffer().put(data);
	        CvMat cvmat = cvMatNv21Image;

	        cvCvtColor(cvmat, cvMatBgrImage, CV_YUV2BGR_NV21);
	        cvmat = cvMatBgrImage;


			if(param.portrait==1){
				int w = param.width;
				int h = param.height;
				if(EncodeType.crop){
					w = param.width/2;
					CvPoint2D32f crop_center = new CvPoint2D32f(w/2, h/2);
					cvGetRectSubPix(cvmat, cvMatCropImage, crop_center);
					cvmat = cvMatCropImage;
				}
				int c = w;
				w = h;
				h = c;
				int rot_center_gt = w/2>h/2 ? w/2 : h/2;
				CvPoint2D32f rot_center = new CvPoint2D32f(rot_center_gt-1, rot_center_gt-1);
		        CvMat map_matrix = cvCreateMat(2, 3, CV_32FC1);
		        CvMat rot = cv2DRotationMatrix(rot_center, -90.0f, 1.0f, map_matrix);
		        cvWarpAffine(cvmat, cvMatBgrRotImage, rot);
		        cvmat = cvMatBgrRotImage;
			}
	        
	        //String sd = Environment.getExternalStorageDirectory().getPath();
			//cvSaveImage(sd+"/test1.jpg", cvMatRgbImage.asIplImage());
			//cvSaveImage(sd+"/test2.jpg", cvMatRgbCropImage.asIplImage());
			//cvSaveImage(sd+"/test3.jpg", cvMatRgbRotImage.asIplImage());

	        return cvmat.asIplImage();
		}
	}
	
	@Override
	public void onAudioBuffer(ShortBuffer buffer) {
		audioRec(buffer);
	}

	public void onCameraPreview(byte[] b) {
		if(!encording){
			videoRec(b);
		}
	}

	public boolean isRecording(){
		return recording;
	}

	private void startVideoEmptyRecordThread(){
		if(videoEmptyRecordThread!=null){
			videoEmptyRecordThread.interrupt();
		}
		videoEmptyRecordThread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					while(true){
						Thread.sleep(1000);
						if(System.currentTimeMillis()-startTime<3000) continue;
						if(recording){
							if(System.currentTimeMillis() - getLatestFrameTime() > 1000){
								videoEmptyRec();
							}
						}
					}
				} catch (InterruptedException e) {
					//e.printStackTrace();
				}
			}
		});
		videoEmptyRecordThread.start();
	}

	private void stopVideoEmptyRecordThread(){
		if(videoEmptyRecordThread!=null){
			videoEmptyRecordThread.interrupt();
			videoEmptyRecordThread = null;
		}
	}

	public int getFps(){
		int count = 0;
		for (int i = 0; i < fpsc.size(); i++) {
			if(System.currentTimeMillis() - fpsc.get(i) < 1000){
				count++;
			}
		}
		return count;
	}
	
	private long getLatestFrameTime(){
		synchronized (fpsc) {
			if(fpsc.size()>0){
				return fpsc.get(fpsc.size()-1);
			}else{
				return 0;
			}
		}
	}
	
	private void addFrameTime(long time){
		fpsc.add(time);
		if(fpsc.size()>30) fpsc.remove(0);
	}

	private void fireFpsEvent(int fps){
		for (int i = 0; i < listeners.size(); i++) {
			listeners.get(i).onEncodeFps(fps);
		}
	}
	
	private void fireEncodeErrorEvent(){
		for (int i = 0; i < listeners.size(); i++) {
			listeners.get(i).onEncodeError();
		}
	}

	public void addListener(OnEncodeListener listener){
		listeners.remove(listener);
		listeners.add(listener);
	}

	public void removeListener(OnEncodeListener listener){
		listeners.remove(listener);
	}
	
}
